/*
 *    Copyright 2009-2014 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.builder;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.decorators.LruCache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

import java.util.*;

/**
 * @author Clinton Begin
 */
public class MapperBuilderAssistant
        extends BaseBuilder {

    private String currentNamespace;
    private String resource;
    private Cache currentCache;
    private boolean unresolvedCacheRef; // issue #676

    public MapperBuilderAssistant(
            Configuration configuration,
            String resource) {
        super(configuration);
        ErrorContext.instance()
                .resource(resource);
        this.resource = resource;
    }

    public String getCurrentNamespace() {
        return currentNamespace;
    }

    public void setCurrentNamespace(
            String currentNamespace) {
        if (currentNamespace == null) {
            throw new BuilderException(
                    "The mapper element requires a namespace attribute to be specified.");
        }

        if (this.currentNamespace != null
                && !this.currentNamespace
                .equals(currentNamespace)) {
            throw new BuilderException(
                    "Wrong namespace. Expected '"
                            + this.currentNamespace
                            + "' but found '"
                            + currentNamespace
                            + "'.");
        }

        this.currentNamespace = currentNamespace;
    }

    public String applyCurrentNamespace(
            String base,
            boolean isReference) {
        if (base == null)
            return null;
        if (isReference) {
            // is it qualified with any namespace yet?
            if (base.contains("."))
                return base;
        } else {
            // is it qualified with this namespace yet?
            if (base.startsWith(currentNamespace
                    + "."))
                return base;
            if (base.contains("."))
                throw new BuilderException(
                        "Dots are not allowed in element names, please remove it from "
                                + base);
        }
        return currentNamespace + "."
                + base;
    }

    public Cache useCacheRef(
            String namespace) {
        if (namespace == null) {
            throw new BuilderException(
                    "cache-ref element requires a namespace attribute.");
        }
        try {
            unresolvedCacheRef = true;
            Cache cache = configuration
                    .getCache(namespace);
            if (cache == null) {
                throw new IncompleteElementException(
                        "No cache for namespace '"
                                + namespace
                                + "' could be found.");
            }
            currentCache = cache;
            unresolvedCacheRef = false;
            return cache;
        } catch (IllegalArgumentException e) {
            throw new IncompleteElementException(
                    "No cache for namespace '"
                            + namespace
                            + "' could be found.",
                    e);
        }
    }

    public Cache useNewCache(
            Class<? extends Cache> typeClass,
            Class<? extends Cache> evictionClass,
            Long flushInterval,
            Integer size,
            boolean readWrite,
            Properties props) {
        typeClass = valueOrDefault(
                typeClass,
                PerpetualCache.class);
        evictionClass = valueOrDefault(
                evictionClass,
                LruCache.class);
        Cache cache = new CacheBuilder(
                currentNamespace)
                .implementation(
                        typeClass)
                .addDecorator(
                        evictionClass)
                .clearInterval(
                        flushInterval)
                .size(size)
                .readWrite(readWrite)
                .properties(props)
                .build();
        configuration.addCache(cache);
        currentCache = cache;
        return cache;
    }

    public ParameterMap addParameterMap(
            String id,
            Class<?> parameterClass,
            List<ParameterMapping> parameterMappings) {
        id = applyCurrentNamespace(id,
                false);
        ParameterMap.Builder parameterMapBuilder = new ParameterMap.Builder(
                configuration, id,
                parameterClass,
                parameterMappings);
        ParameterMap parameterMap = parameterMapBuilder
                .build();
        configuration
                .addParameterMap(parameterMap);
        return parameterMap;
    }

    public ParameterMapping buildParameterMapping(
            Class<?> parameterType,
            String property,
            Class<?> javaType,
            JdbcType jdbcType,
            String resultMap,
            ParameterMode parameterMode,
            Class<? extends TypeHandler<?>> typeHandler,
            Integer numericScale) {
        resultMap = applyCurrentNamespace(
                resultMap, true);

        // Class parameterType = parameterMapBuilder.type();
        Class<?> javaTypeClass = resolveParameterJavaType(
                parameterType,
                property, javaType,
                jdbcType);
        TypeHandler<?> typeHandlerInstance = resolveTypeHandler(
                javaTypeClass,
                typeHandler);

        ParameterMapping.Builder builder = new ParameterMapping.Builder(
                configuration,
                property, javaTypeClass);
        builder.jdbcType(jdbcType);
        builder.resultMapId(resultMap);
        builder.mode(parameterMode);
        builder.numericScale(numericScale);
        builder.typeHandler(typeHandlerInstance);
        return builder.build();
    }

    public ResultMap addResultMap(
            String id,
            Class<?> type,
            String extend,
            Discriminator discriminator,
            List<ResultMapping> resultMappings,
            Boolean autoMapping) {
        id = applyCurrentNamespace(id,
                false);
        extend = applyCurrentNamespace(
                extend, true);

        ResultMap.Builder resultMapBuilder = new ResultMap.Builder(
                configuration, id,
                type, resultMappings,
                autoMapping);
        if (extend != null) {
            if (!configuration
                    .hasResultMap(extend)) {
                throw new IncompleteElementException(
                        "Could not find a parent resultmap with id '"
                                + extend
                                + "'");
            }
            ResultMap resultMap = configuration
                    .getResultMap(extend);
            List<ResultMapping> extendedResultMappings = new ArrayList<ResultMapping>(
                    resultMap
                            .getResultMappings());
            extendedResultMappings
                    .removeAll(resultMappings);
            // Remove parent constructor if this resultMap declares a
            // constructor.
            boolean declaresConstructor = false;
            for (ResultMapping resultMapping : resultMappings) {
                if (resultMapping
                        .getFlags()
                        .contains(
                                ResultFlag.CONSTRUCTOR)) {
                    declaresConstructor = true;
                    break;
                }
            }
            if (declaresConstructor) {
                Iterator<ResultMapping> extendedResultMappingsIter = extendedResultMappings
                        .iterator();
                while (extendedResultMappingsIter
                        .hasNext()) {
                    if (extendedResultMappingsIter
                            .next()
                            .getFlags()
                            .contains(
                                    ResultFlag.CONSTRUCTOR)) {
                        extendedResultMappingsIter
                                .remove();
                    }
                }
            }
            resultMappings
                    .addAll(extendedResultMappings);
        }
        resultMapBuilder
                .discriminator(discriminator);
        ResultMap resultMap = resultMapBuilder
                .build();
        configuration
                .addResultMap(resultMap);
        return resultMap;
    }

    public Discriminator buildDiscriminator(
            Class<?> resultType,
            String column,
            Class<?> javaType,
            JdbcType jdbcType,
            Class<? extends TypeHandler<?>> typeHandler,
            Map<String, String> discriminatorMap) {
        ResultMapping resultMapping = buildResultMapping(
                resultType,
                null,
                column,
                javaType,
                jdbcType,
                null,
                null,
                null,
                null,
                typeHandler,
                new ArrayList<ResultFlag>(),
                null, null, false);
        Map<String, String> namespaceDiscriminatorMap = new HashMap<String, String>();
        for (Map.Entry<String, String> e : discriminatorMap
                .entrySet()) {
            String resultMap = e
                    .getValue();
            resultMap = applyCurrentNamespace(
                    resultMap, true);
            namespaceDiscriminatorMap
                    .put(e.getKey(),
                            resultMap);
        }
        Discriminator.Builder discriminatorBuilder = new Discriminator.Builder(
                configuration,
                resultMapping,
                namespaceDiscriminatorMap);
        return discriminatorBuilder
                .build();
    }

    public MappedStatement addMappedStatement(
            String id,
            SqlSource sqlSource,
            StatementType statementType,
            SqlCommandType sqlCommandType,
            Integer fetchSize,
            Integer timeout,
            String parameterMap,
            Class<?> parameterType,
            String resultMap,
            Class<?> resultType,
            ResultSetType resultSetType,
            boolean flushCache,
            boolean useCache,
            boolean resultOrdered,
            KeyGenerator keyGenerator,
            String keyProperty,
            String keyColumn,
            String databaseId,
            LanguageDriver lang,
            String resultSets) {

        if (unresolvedCacheRef)
            throw new IncompleteElementException(
                    "Cache-ref not yet resolved");

        id = applyCurrentNamespace(id,
                false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(
                configuration, id,
                sqlSource,
                sqlCommandType);
        statementBuilder
                .resource(resource);
        statementBuilder
                .fetchSize(fetchSize);
        statementBuilder
                .statementType(statementType);
        statementBuilder
                .keyGenerator(keyGenerator);
        statementBuilder
                .keyProperty(keyProperty);
        statementBuilder
                .keyColumn(keyColumn);
        statementBuilder
                .databaseId(databaseId);
        statementBuilder.lang(lang);
        statementBuilder
                .resultOrdered(resultOrdered);
        statementBuilder
                .resulSets(resultSets);
        setStatementTimeout(timeout,
                statementBuilder);

        setStatementParameterMap(
                parameterMap,
                parameterType,
                statementBuilder);
        setStatementResultMap(
                resultMap, resultType,
                resultSetType,
                statementBuilder);
        setStatementCache(isSelect,
                flushCache, useCache,
                currentCache,
                statementBuilder);

        MappedStatement statement = statementBuilder
                .build();
        configuration
                .addMappedStatement(statement);
        return statement;
    }

    private <T> T valueOrDefault(
            T value, T defaultValue) {
        return value == null ? defaultValue
                : value;
    }

    private void setStatementCache(
            boolean isSelect,
            boolean flushCache,
            boolean useCache,
            Cache cache,
            MappedStatement.Builder statementBuilder) {
        flushCache = valueOrDefault(
                flushCache, !isSelect);
        useCache = valueOrDefault(
                useCache, isSelect);
        statementBuilder
                .flushCacheRequired(flushCache);
        statementBuilder
                .useCache(useCache);
        statementBuilder.cache(cache);
    }

    private void setStatementParameterMap(
            String parameterMap,
            Class<?> parameterTypeClass,
            MappedStatement.Builder statementBuilder) {
        parameterMap = applyCurrentNamespace(
                parameterMap, true);

        if (parameterMap != null) {
            try {
                statementBuilder
                        .parameterMap(configuration
                                .getParameterMap(parameterMap));
            } catch (IllegalArgumentException e) {
                throw new IncompleteElementException(
                        "Could not find parameter map "
                                + parameterMap,
                        e);
            }
        } else if (parameterTypeClass != null) {
            List<ParameterMapping> parameterMappings = new ArrayList<ParameterMapping>();
            ParameterMap.Builder inlineParameterMapBuilder = new ParameterMap.Builder(
                    configuration,
                    statementBuilder
                            .id()
                            + "-Inline",
                    parameterTypeClass,
                    parameterMappings);
            statementBuilder
                    .parameterMap(inlineParameterMapBuilder
                            .build());
        }
    }

    private void setStatementResultMap(
            String resultMap,
            Class<?> resultType,
            ResultSetType resultSetType,
            MappedStatement.Builder statementBuilder) {
        resultMap = applyCurrentNamespace(
                resultMap, true);

        List<ResultMap> resultMaps = new ArrayList<ResultMap>();
        if (resultMap != null) {
            String[] resultMapNames = resultMap
                    .split(",");
            for (String resultMapName : resultMapNames) {
                try {
                    resultMaps
                            .add(configuration
                                    .getResultMap(resultMapName
                                            .trim()));
                } catch (IllegalArgumentException e) {
                    throw new IncompleteElementException(
                            "Could not find result map "
                                    + resultMapName,
                            e);
                }
            }
        } else if (resultType != null) {
            ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
                    configuration,
                    statementBuilder
                            .id()
                            + "-Inline",
                    resultType,
                    new ArrayList<ResultMapping>(),
                    null);
            resultMaps
                    .add(inlineResultMapBuilder
                            .build());
        }
        statementBuilder
                .resultMaps(resultMaps);

        statementBuilder
                .resultSetType(resultSetType);
    }

    private void setStatementTimeout(
            Integer timeout,
            MappedStatement.Builder statementBuilder) {
        if (timeout == null) {
            timeout = configuration
                    .getDefaultStatementTimeout();
        }
        statementBuilder
                .timeout(timeout);
    }

    public ResultMapping buildResultMapping(
            Class<?> resultType,
            String property,
            String column,
            Class<?> javaType,
            JdbcType jdbcType,
            String nestedSelect,
            String nestedResultMap,
            String notNullColumn,
            String columnPrefix,
            Class<? extends TypeHandler<?>> typeHandler,
            List<ResultFlag> flags,
            String resultSet,
            String foreignColumn,
            boolean lazy) {
        Class<?> javaTypeClass = resolveResultJavaType(
                resultType, property,
                javaType);
        TypeHandler<?> typeHandlerInstance = resolveTypeHandler(
                javaTypeClass,
                typeHandler);
        List<ResultMapping> composites = parseCompositeColumnName(column);
        if (composites.size() > 0)
            column = null;
        ResultMapping.Builder builder = new ResultMapping.Builder(
                configuration,
                property, column,
                javaTypeClass);
        builder.jdbcType(jdbcType);
        builder.nestedQueryId(applyCurrentNamespace(
                nestedSelect, true));
        builder.nestedResultMapId(applyCurrentNamespace(
                nestedResultMap, true));
        builder.resultSet(resultSet);
        builder.typeHandler(typeHandlerInstance);
        builder.flags(flags == null ? new ArrayList<ResultFlag>()
                : flags);
        builder.composites(composites);
        builder.notNullColumns(parseMultipleColumnNames(notNullColumn));
        builder.columnPrefix(columnPrefix);
        builder.foreignColumn(foreignColumn);
        builder.lazy(lazy);
        return builder.build();
    }

    private Set<String> parseMultipleColumnNames(
            String columnName) {
        Set<String> columns = new HashSet<String>();
        if (columnName != null) {
            if (columnName.indexOf(',') > -1) {
                StringTokenizer parser = new StringTokenizer(
                        columnName,
                        "{}, ", false);
                while (parser
                        .hasMoreTokens()) {
                    String column = parser
                            .nextToken();
                    columns.add(column);
                }
            } else {
                columns.add(columnName);
            }
        }
        return columns;
    }

    private List<ResultMapping> parseCompositeColumnName(
            String columnName) {
        List<ResultMapping> composites = new ArrayList<ResultMapping>();
        if (columnName != null
                && (columnName
                .indexOf('=') > -1 || columnName
                .indexOf(',') > -1)) {
            StringTokenizer parser = new StringTokenizer(
                    columnName,
                    "{}=, ", false);
            while (parser
                    .hasMoreTokens()) {
                String property = parser
                        .nextToken();
                String column = parser
                        .nextToken();
                ResultMapping.Builder complexBuilder = new ResultMapping.Builder(
                        configuration,
                        property,
                        column,
                        configuration
                                .getTypeHandlerRegistry()
                                .getUnknownTypeHandler());
                composites
                        .add(complexBuilder
                                .build());
            }
        }
        return composites;
    }

    private Class<?> resolveResultJavaType(
            Class<?> resultType,
            String property,
            Class<?> javaType) {
        if (javaType == null
                && property != null) {
            try {
                MetaClass metaResultType = MetaClass
                        .forClass(resultType);
                javaType = metaResultType
                        .getSetterType(property);
            } catch (Exception e) {
                // ignore, following null check statement will deal with the
                // situation
            }
        }
        if (javaType == null) {
            javaType = Object.class;
        }
        return javaType;
    }

    private Class<?> resolveParameterJavaType(
            Class<?> resultType,
            String property,
            Class<?> javaType,
            JdbcType jdbcType) {
        if (javaType == null) {
            if (JdbcType.CURSOR
                    .equals(jdbcType)) {
                javaType = java.sql.ResultSet.class;
            } else if (Map.class
                    .isAssignableFrom(resultType)) {
                javaType = Object.class;
            } else {
                MetaClass metaResultType = MetaClass
                        .forClass(resultType);
                javaType = metaResultType
                        .getGetterType(property);
            }
        }
        if (javaType == null) {
            javaType = Object.class;
        }
        return javaType;
    }

    /**
     * Backward compatibility signature
     */
    public ResultMapping buildResultMapping(
            Class<?> resultType,
            String property,
            String column,
            Class<?> javaType,
            JdbcType jdbcType,
            String nestedSelect,
            String nestedResultMap,
            String notNullColumn,
            String columnPrefix,
            Class<? extends TypeHandler<?>> typeHandler,
            List<ResultFlag> flags) {
        return buildResultMapping(
                resultType,
                property,
                column,
                javaType,
                jdbcType,
                nestedSelect,
                nestedResultMap,
                notNullColumn,
                columnPrefix,
                typeHandler,
                flags,
                null,
                null,
                configuration
                        .isLazyLoadingEnabled());
    }

    public LanguageDriver getLanguageDriver(
            Class<?> langClass) {
        if (langClass != null) {
            configuration
                    .getLanguageRegistry()
                    .register(langClass);
        } else {
            langClass = configuration
                    .getLanguageRegistry()
                    .getDefaultDriverClass();
        }
        return configuration
                .getLanguageRegistry()
                .getDriver(langClass);
    }

    /**
     * Backward compatibility signature
     */
    public MappedStatement addMappedStatement(
            String id,
            SqlSource sqlSource,
            StatementType statementType,
            SqlCommandType sqlCommandType,
            Integer fetchSize,
            Integer timeout,
            String parameterMap,
            Class<?> parameterType,
            String resultMap,
            Class<?> resultType,
            ResultSetType resultSetType,
            boolean flushCache,
            boolean useCache,
            boolean resultOrdered,
            KeyGenerator keyGenerator,
            String keyProperty,
            String keyColumn,
            String databaseId,
            LanguageDriver lang) {
        return addMappedStatement(id,
                sqlSource,
                statementType,
                sqlCommandType,
                fetchSize, timeout,
                parameterMap,
                parameterType,
                resultMap, resultType,
                resultSetType,
                flushCache, useCache,
                resultOrdered,
                keyGenerator,
                keyProperty, keyColumn,
                databaseId, lang, null);
    }

}
